import subprocess
import threading
import os
import signal
import time
from src.utils.logger import get_logger

class ProcessManager:
    def __init__(self):
        self.process = None
        self.logger = get_logger(__name__)
        self._lock = threading.Lock()

    def run(self, command, log_callback=None, time_update_callback=None, cwd=None):
        with self._lock:
            if self.process and self.process.poll() is None:
                self.logger.warning("A process is already running.")
                raise RuntimeError("A process is already running.")

            if log_callback:
                log_callback(f"Executing command: {' '.join(command)}")
            else:
                self.logger.debug(f"Executing command: {' '.join(command)}")

            # Use os.setsid on Unix to create a new process group.
            # This allows us to kill the command and all its children.
            preexec_fn = os.setsid if os.name != 'nt' else None

            self.process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                encoding='utf-8',
                errors='replace',
                preexec_fn=preexec_fn,
                cwd=cwd
            )

        def log_output():
            for line in iter(self.process.stdout.readline, ''):
                if line:
                    clean_line = line.strip()
                    if log_callback:
                        log_callback(clean_line)
                    else:
                        self.logger.debug(clean_line)

        log_thread = threading.Thread(target=log_output)
        log_thread.daemon = True
        log_thread.start()

        time_thread = None
        if time_update_callback:
            def time_updater():
                while self.process and self.process.poll() is None:
                    time_update_callback()
                    time.sleep(1)
            
            time_thread = threading.Thread(target=time_updater)
            time_thread.daemon = True
            time_thread.start()

        returncode = self.process.wait()
        log_thread.join(timeout=1)
        if time_thread:
            time_thread.join(timeout=1)

        with self._lock:
            self.process = None

        # A negative return code on Unix means the process was terminated by a signal.
        # We consider this a "clean" exit if we requested the stop.
        if returncode != 0 and returncode not in [-signal.SIGTERM, -signal.SIGKILL]:
            error_message = f"Command execution failed with return code: {returncode}"
            if log_callback:
                log_callback(error_message)
            else:
                self.logger.error(error_message)
            raise subprocess.CalledProcessError(returncode, command)
        
        return returncode

    def stop(self):
        with self._lock:
            if self.process and self.process.poll() is None:
                self.logger.debug(f"Attempting to stop process group {self.process.pid}...")
                try:
                    if os.name == 'nt':
                        # On Windows, taskkill is more reliable for killing process trees.
                        subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], check=True, capture_output=True)
                    else:
                        # On Unix, kill the entire process group.
                        os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
                    
                    self.logger.debug(f"Termination signal sent to process group {self.process.pid}.")
                except (ProcessLookupError, PermissionError, subprocess.CalledProcessError) as e:
                    self.logger.error(f"Failed to send termination signal to process {self.process.pid}: {e}")
                # The wait() in the run method will handle the rest.
            else:
                self.logger.debug("No active process to stop.")

# Keep a compatible run_command function for parts of the code that don't need stopping.
# This is for short-lived commands that don't need external interruption.
def run_command(command, log_callback=None):
    logger = get_logger(__name__)
    
    if log_callback:
        log_callback(f"Executing command: {' '.join(command)}")
    else:
        logger.debug(f"Executing command: {' '.join(command)}")

    process = subprocess.Popen(
        command,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        encoding='utf-8',
        errors='replace'
    )

    def log_output():
        for line in iter(process.stdout.readline, ''):
            if line:
                clean_line = line.strip()
                if log_callback:
                    log_callback(clean_line)
                else:
                    logger.debug(clean_line)

    log_thread = threading.Thread(target=log_output)
    log_thread.start()
    
    process.wait()
    log_thread.join()

    if process.returncode != 0:
        error_message = f"Command execution failed, return code: {process.returncode}"
        if log_callback:
            log_callback(error_message)
        else:
            logger.error(error_message)
        raise subprocess.CalledProcessError(process.returncode, command)

    return process.returncode